package com.meego.common.oembed;

import com.meego.common.html.HTMLParser;
import com.meego.common.html.nodes.Document;
import com.meego.common.html.nodes.Element;
import com.meego.common.html.nodes.Entities.EscapeMode;
import com.meego.common.log.GA;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.util.EntityUtils;

public class Oembed {

    /**
     * The HttpClient instance for all requests. It should be configured for
     * multithreading
     */
    private final HttpClient httpClient;
    /**
     * The map of known providers
     */
    private Map<String, OembedProvider> provider = new HashMap<String, OembedProvider>();
    /**
     * Optional handlers for providers registered in {@link #provider}
     */
    private Map<String, OembedResponseHandler> handler = new HashMap<String, OembedResponseHandler>();
    /**
     * The map of all known parsers. For now, only json and xml exists
     */
    private Map<String, OembedParser> parser;
    /**
     * Flag, if autodiscovery is enabled when there is no provider for a
     * specific url. Defaults to false
     */
    private boolean autodiscovery = false;
    /**
     * If this is not null and <tt>cacheManager</tt> is not null, failed urls
     * aren't called for the given amount of seconds
     */
    private Long ignoreFailedUrlsForSeconds = new Long(24 * 60 * 60);
    private String baseUri = "";
    /**
     * Name of the ehcache, defaults to the fully qualified name of Oembed
     */
    private String cacheName = Oembed.class.getName();
    /**
     * The default user agent
     */
    private String userAgent;
    /**
     * An optional string that is appended to the user agent
     */
    private String consumer;

    public HttpClient getHttpClient() {
        return httpClient;
    }

    /**
     * Constructs the Oembed Api with the default parsers (json and xml) and an
     * empty map of provider
     *
     * @param httpClient
     */
    public Oembed(final HttpClient httpClient) {
        this.httpClient = httpClient;

        this.parser = new HashMap<String, OembedParser>();
        this.parser.put("json", new OembedJsonParser());
        this.parser.put("xml", new OembedXmlParser());

        final Properties version = new Properties();
        version.put("ac.simons.oembed.version", "1.0");
        this.userAgent = String.format("Java/%s java-oembed/%s", System.getProperty("java.version"), version.getProperty("ac.simons.oembed.version"));
        GA.plugin.info(String.format("Oembed (%s) ready...", this.userAgent));
    }

    public Map<String, OembedProvider> getProvider() {
        return provider;
    }

    public void setProvider(Map<String, OembedProvider> provider) {
        this.provider = provider;
    }

    public OembedParser getParser(final String format) {
        return this.parser.get(format);
    }

    /**
     * Transforms the given URL into an OembedResponse. Returns null if there is
     * no provider configured for this url.
     *
     * @param url
     * @return
     * @throws OembedException
     */
    public OembedResponse transformUrl(final String url) throws OembedException {
        OembedResponse response = null;
        if (url == null || url.length() == 0) {
            GA.plugin.warn("Can't embed an empty url!");
        } else {
            if (OEmbedCA.isCacheSupport) {
                try {
                    GA.plugin.debug("Trying to use ehcache");
                    response = OEmbedCA.get(url);
                } catch (Exception e) {
                    GA.plugin.warn(String.format("There was a problem with ehcache: %s", e.getMessage()), e);
                }
            }

            if (response != null) {
                GA.plugin.debug("Using cached result...");
                if (response.isEmpty()) {
                    response = null;
                }
            } else {
                OembedProvider provider = this.findProvider(url);
                if (provider == null && (!this.isAutodiscovery() || (provider = autodiscoverOembedURIForUrl(url)) == null)) {
                    GA.plugin.info(String.format("No oembed provider for url %s and autodiscovery is disabled or found no result", url));
                } else {
                    try {
                        final URI api = provider.toApiUrl(url);
                        GA.plugin.debug(String.format("Calling url %s", api.toString()));
                        final HttpGet request = new HttpGet(api);
                        if (this.userAgent != null) {
                            request.setHeader("User-Agent", String.format("%s%s", this.userAgent, this.consumer == null ? "" : "; " + this.consumer));
                        }
                        final HttpResponse httpResponse = this.httpClient.execute(request);
                        if (httpResponse.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
                            GA.plugin.warn(String.format("Server returned error %d for '%s': %s", httpResponse.getStatusLine().getStatusCode(), url, EntityUtils.toString(httpResponse.getEntity())));
                            if (ignoreFailedUrlsForSeconds != null && OEmbedCA.isCacheSupport) {
                                final OembedResponse emptyResponse = new OembedResponse();
                                emptyResponse.setEmpty(true);
                                emptyResponse.setCacheAge(ignoreFailedUrlsForSeconds);
                                OEmbedCA.insert(url, emptyResponse);
                            }
                        } else {
                            response = this.getParser(provider.getFormat().toLowerCase()).unmarshal(httpResponse.getEntity().getContent());
                            response.setSource(provider.getName());
                            response.setOriginalUrl(url);
                            if (OEmbedCA.isCacheSupport) {
                                try {
                                    OEmbedCA.insert(url, response);
                                } catch (Exception e) {
                                    GA.plugin.warn(String.format("Could not cache response for %s: %s", url, e.getMessage(), e));
                                }
                            }
                        }
                    } catch (IOException e) {
                        throw new OembedException(e);
                    } catch (NullPointerException e) {
                        throw new OembedException(String.format("NPE, probably invalid format :%s", provider.getFormat()));
                    } catch (URISyntaxException e) {
                        throw new OembedException(e);
                    } catch (Exception e) {
                        throw new OembedException(e);
                    }
                }
            }
        }

        return response;
    }

    public String transformDocumentString(final String documentHtml) {
        final Document rv = transformDocument(documentHtml);
        rv.outputSettings().prettyPrint(false).escapeMode(EscapeMode.xhtml);
        return rv.body().html();
    }

    public Document transformDocument(final String documentHtml) {
        return transformDocument(HTMLParser.parseBodyFragment(documentHtml, baseUri));
    }

    /**
     * Parses the given html document into a document and processes all anchor
     * elements. If a valid anchor is found, it tries to get an oembed response
     * for it's url and than render the result into the document replacing the
     * given anchor.<br> It returns the html representation of the new
     * document.<br> If there's an error or no oembed result for an url, the
     * anchor tag will be left as it was.
     *
     * @param documentHtml
     * @return
     */
    public Document transformDocument(final Document document) {
        boolean changedBaseUri = false;
        if (document.baseUri() == null && this.getBaseUri() != null) {
            document.setBaseUri(this.getBaseUri());
            changedBaseUri = true;
        }
        for (Element a : document.getElementsByTag("a")) {
            final String href = a.absUrl("href");
            try {
                String renderedRespose;
                final OembedResponse oembedResponse = this.transformUrl(href);
                // There was no response or an exception happened
                if (oembedResponse == null) {
                    continue;
                } // There is a handler for this response
                else if (this.getHandler().containsKey(oembedResponse.getSource())) {
                    this.getHandler().get(oembedResponse.getSource()).handle(document, a, oembedResponse);
                } // Try to render the response itself and replace the current anchor
                else if ((renderedRespose = oembedResponse.render()) != null) {
                    a.before(renderedRespose);
                    a.remove();
                }
            } catch (OembedException e) {
            }
        }
        if (changedBaseUri) {
            document.setBaseUri(null);
        }
        return document;
    }

    /**
     * Finds a provider for the given url
     *
     * @param url
     * @return
     */
    private OembedProvider findProvider(final String url) {
        OembedProvider rv = null;
        providerLoop:
        for (OembedProvider provider : this.provider.values()) {
            for (String urlScheme : provider.getUrlSchemes()) {
                System.out.println(url);
                System.out.println(urlScheme);
                System.out.println(url.matches(urlScheme));
                if (url.matches(urlScheme)) {
                    rv = provider;
                    break providerLoop;
                }
            }
        }
        return rv;
    }

    private OembedProvider autodiscoverOembedURIForUrl(final String url) {
        OembedProvider rv = null;

        try {
            final HttpGet request = new HttpGet(url);
            final HttpResponse httpResponse = this.httpClient.execute(request);
            if (httpResponse.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
                GA.plugin.warn(String.format("Autodiscovery for %s failed, server returned error %d: %s", url, httpResponse.getStatusLine().getStatusCode(), EntityUtils.toString(httpResponse.getEntity())));
            } else {
                final URI uri = request.getURI();
                final Document document = HTMLParser.parse(EntityUtils.toString(httpResponse.getEntity(), "UTF-8"), String.format("%s://%s:%d", uri.getScheme(), uri.getHost(), uri.getPort()));
                for (Element alternate : document.getElementsByAttributeValue("rel", "alternate")) {
                    if (alternate.attr("type").equalsIgnoreCase("application/json+oembed")) {
                        rv = new AutodiscoveredOembedProvider(url, new URI(alternate.absUrl("href")), "json");
                    } else if (alternate.attr("type").equalsIgnoreCase("text/xml+oembed")) {
                        rv = new AutodiscoveredOembedProvider(url, new URI(alternate.absUrl("href")), "xml");
                    }
                    if (rv != null) {
                        break;
                    }
                }
            }
        } catch (Exception e) {
            GA.plugin.warn(String.format("Autodiscovery for %s failedd: %s", url, e.getMessage()), e);
        }

        return rv;
    }

    public Map<String, OembedResponseHandler> getHandler() {
        return handler;
    }

    public void setHandler(Map<String, OembedResponseHandler> handler) {
        this.handler = handler;
    }

    public Map<String, OembedParser> getParser() {
        return parser;
    }

    public void setParser(Map<String, OembedParser> parser) {
        this.parser = parser;
    }

    public boolean isAutodiscovery() {
        return autodiscovery;
    }

    public void setAutodiscovery(boolean autodiscovery) {
        this.autodiscovery = autodiscovery;
    }

    public String getBaseUri() {
        return baseUri;
    }

    public void setBaseUri(String baseUri) {
        this.baseUri = baseUri;
    }

    public String getCacheName() {
        return cacheName;
    }

    public void setCacheName(String cacheName) {
        this.cacheName = cacheName;
    }

//	protected OembedResponse getFromCache(final String key) {
//		Ehcache cache = this.cacheManager.getCache(this.cacheName);
//		if(cache == null) {
//			this.cacheManager.addCache(this.cacheName);
//			cache = this.cacheManager.getCache(this.cacheName);
//		}
//		OembedResponse rv = null;
//		final net.sf.ehcache.Element element = cache.get(key);
//		if(element != null)
//			rv = (OembedResponse) element.getValue();
//		return rv;
//	}	
//	
//	protected void addToCache(final String url, final OembedResponse response) {
//		final Ehcache cache = this.cacheManager.getCache(this.cacheName);
//		
//		cache.put(new net.sf.ehcache.Element(url, response, null, null, response.getCacheAge() != null ? new Long(Math.min(Math.max(MIN_VALID_CACHE_AGE, response.getCacheAge()), MAX_VALID_CACHE_AGE)).intValue() : this.getDefaultCacheAge()));
//	}
    public Long getIgnoreFailedUrlsForSeconds() {
        return ignoreFailedUrlsForSeconds;
    }

    public void setIgnoreFailedUrlsForSeconds(Long ignoreFailedUrlsForSeconds) {
        this.ignoreFailedUrlsForSeconds = ignoreFailedUrlsForSeconds;
    }

    public String getUserAgent() {
        return userAgent;
    }

    public void setUserAgent(String userAgent) {
        this.userAgent = userAgent;
    }

    public String getConsumer() {
        return consumer;
    }

    public void setConsumer(String consumer) {
        this.consumer = consumer;
    }
}